import numpy as np
import matplotlib.pyplot as plt

# ----------------------------------------------------------------------------
# 1. 定义两类散点
# 定义第一类样本点
class1_points = np.array([[1.9, 1.2],
                          [1.5, 2.1],
                          [1.9, 0.5],
                          [1.5, 0.9],
                          [0.9, 1.2],
                          [1.1, 1.7],
                          [1.4, 1.1]])

# 定义第二类样本点
class2_points = np.array([[3.2, 3.2],
                          [3.7, 2.9],
                          [3.2, 2.6],
                          [1.7, 3.3],
                          [3.4, 2.6],
                          [4.1, 2.3],
                          [3.0, 2.9]])
x1 = np.concatenate((class1_points[:, 0], class2_points[:, 0]), axis=0)
x2 = np.concatenate((class1_points[:, 1], class2_points[:, 1]), axis=0)
label = np.concatenate(
    (np.zeros(len(class1_points)), np.ones(len(class2_points))),
    axis=0
)
"""
print(x1)
print(x2)
print(label)
[1.9 1.5 1.9 1.5 0.9 1.1 1.4 3.2 3.7 3.2 1.7 3.4 4.1 3. ]
[1.2 2.1 0.5 0.9 1.2 1.7 1.1 3.2 2.9 2.6 3.3 2.6 2.3 2.9]
[0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1.]
"""


# 3. 激活函数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))


# 2. 期望函数（数学模型）
def f(w1, w2, x1, x2, b):
    z = w1 * x1 + w2 * x2 + b
    a = sigmoid(z)
    return a


# 4. 损失函数
def loss_fn(a, label):
    """
    交叉熵损失函数 CrossEntropyLoss
    :param a:   sigmoid()函数值，也就是我们说的概率值，预测标签
    :param label:   真实标签
    """
    return -np.mean(label * np.log(a) + (1 - label) * np.log(1 - a))


# 5. 超参数 和 参数初始化
lr = 0.1
Epochs = 1000
w1 = 0
w2 = 0
b = 0

"""
准备画图 —— 绘制散点分类图ax1 和 损失下降图ax2
"""
fig, (ax1, ax2) = plt.subplots(1, 2)
epoch_list = []
loss_list = []

# 6. 循环训练
for epoch in range(Epochs):
    # 前向传播
    a = f(w1, w2, x1, x2, b)
    # 计算损失
    loss = loss_fn(a, label)
    # 反向传播（求导）e = label*np.log(a) + (1-label)*np.log(1-a)
    deda = (label - a) / (a * (1 - a))
    dadz = a * (1 - a)
    dzdw1 = x1
    dzdw2 = x2
    dzdb = 1
    gd_w1 = -np.mean(deda * dadz * dzdw1)
    gd_w2 = -np.mean(deda * dadz * dzdw2)
    gd_b = -np.mean(deda * dadz * dzdb)
    # 更新参数
    w1 = w1 - lr * gd_w1
    w2 = w2 - lr * gd_w2
    b = b - lr * gd_b

    epoch_list.append(epoch)
    loss_list.append(loss)
    # 打印训练信息
    if epoch == 0 or (epoch + 1) % 10 == 0:
        print(f"[{epoch + 1}/{Epochs}] Loss:{round(loss, 3)}")
        # 绘制散点分类图
        ax1.clear()
        ax1.scatter(class1_points[:, 0], class1_points[:, 1], color="r")
        ax1.scatter(class2_points[:, 0], class2_points[:, 1], color="b")
        '''
        sigmoid函数一般用于二分类，不管你输入的是正值还是负值，最终都转换为(0,1)的输出
        怎么划分两类：通常设定一个阈值（如0.5），如果Sigmoid函数的输出大于该阈值，
        则预测为正类（y=1），否则预测为负类（y=0）
        当：sigmoid(w1*x1 + w2*x2 + b) = 0.5时，w1*x1 + w2*x2 + b = 0
        '''
        x1_min, x1_max = x1.min(), x1.max()
        x2_min, x2_max = -(w1 * x1_min + b) / w2, -(w1 * x1_max + b) / w2
        ax1.plot([x1_min, x1_max], [x2_min, x2_max], color="g")

        # 绘制损失下降图
        ax2.clear()
        ax2.plot(epoch_list, loss_list)

        # 暂停一下
        # plt.pause(0.1)

# 7. 画图
plt.show()
print(w1, w2, b)  # 1.2994479497707314 1.6449197299833431 -6.0735703286130125

# ----------------------------------------------------------------------------
'''
TP  真正例    实际为1 预测为1
TN  真负例    实际为0 预测为0
FP  假正例    实际为0 预测为1
FN  假负例    实际为1 预测为0
'''
# ‌准确率（Accuracy）‌——所有样本中，预测正确的
# = (TP+TN) / (TP+TN+FP+FN)
a = f(w1, w2, x1, x2, b)  # 数学模型的输出，已转换为概率值
y_pre = (a >= 0.5).astype(int)
print(label)
print(y_pre)
print(label == y_pre)
acc = np.mean(label == y_pre)
print(f"准确率：{acc * 100:.2f}%")


# F1分数（F1-Score）—— 精确率和召回率的调和平均
# 2 * 精确率 * 召回率 / (精确率+召回率)
# 精确率：预测为真的有多少是真的真      TP/(TP+FP)
# 召回率：实际为真的有多少被找到了      TP/(TP+FN)
def f1_score(TP, FP, FN):
    precision = TP / (TP + FP) if (TP + FP) != 0 else 0.0  # 规避分母为0的报错
    recall = TP / (TP + FN) if (TP + FN) != 0 else 0.0
    if precision + recall == 0:
        return 0.0
    return 2 * precision * recall / (precision + recall)


TP = np.sum((label == 1) & (y_pre == 1))
FP = np.sum((label == 0) & (y_pre == 1))
FN = np.sum((label == 1) & (y_pre == 0))
TN = np.sum((label == 0) & (y_pre == 0))

f1 = f1_score(TP, FP, FN)
print(f"f1分数为：{f1:.4f}")

# 混淆矩阵——汇总TP、FP、TN、FN数量的表格
confusion_matrix = np.array([
    [TN, FP],
    [FN, TP]
])
plt.imshow(confusion_matrix, cmap="Blues")
plt.show()

# ----------------------------------------------------------------------------
# 模型训练好了，也进行评估，感觉不错，我们可以保存下来
# 下面还可以做测试
test1 = np.array([[1.5, 1.5], [5.5, 2.5]])
# 学生练习，输出它的类别 label
a = f(w1, w2, test1[:, 0], test1[:, 1], b)
test1_pre = (a >= 0.5).astype(int)
print(test1_pre)
